iT邦幫忙

2022 iThome 鐵人賽

DAY 15
0
Modern Web

開始搞懂React生態系系列 第 15

Day 15 Memorized Hook - useMemo、useCallback

  • 分享至 

  • xImage
  •  

Re-Render (重新渲染) 及 Memorize (記住)

React 的頁面是由很多個 Component 堆疊而成,在前面的文章可以發現,要讓資料與畫面連動的方式就是觸發 Render,讓畫面重新渲染。

使用少量的元件時,畫面重新渲染不會有太大的問題,一旦網站架構變得龐大且複雜時,大量的元不斷的重新被渲染,可能會給瀏覽器帶給相當大的負擔,也容易造成在使用網站時的 UX 體驗不好。

Re-Render 造成的影響

  1. 重新執行不該被再次執行的運算
  2. 不必要被更新的元件被重新渲染

要減少上述的影響,就是物件要能夠被 Memorize (記住)

Memorized Hook - useMemo、useCallback

React 有兩個 Hook 可以做 Memorize - useMemo、useCallback,它們讓 React 可以記住 Object 的記憶體位址,而不會重新執行不該被再次執行的運算。

為什麼物件 (Object) 要被記住

讓我們來觀察以下二個範例的不同

  • useEffect 的 deps 若為 string/number/boolean,重新 Render 時,aa 值都會一樣,所以不會重新執行 useEffect。
function App() {
  console.log('render');
  const [value, setValue] = React.useState(false);
  const aa = 100;
  React.useEffect(() => { 
    console.log('useEffect cb');
  }, [aa]);

  return <>
    <h1>App</h1>
    <button onClick={() => {setValue(!value)}}>重新Render</button>
  </>
}

執行結果:https://codepen.io/lala-lee-jobs/pen/BaxpaqG?editors=0011

  • useEffect 的 deps 若為「物件(Object)」,重新 Render 時,因為物件的記憶體位置不同,所以會重新執行 useEffect。
function App() {
  console.log('render');
  const [value, setValue] = React.useState(false);
  const obj = { name: 'bruce', data: {} }; 
  React.useEffect(() => { 
    console.log('useEffect cb');
  }, [obj]);

  return <>
    <h1>App</h1>
    <button onClick={() => {setValue(!value)}}>重新Render</button>
  </>
}

執行結果:https://codepen.io/lala-lee-jobs/pen/gOzgyGq?editors=0011

使用 useMemo 記憶物件,避免重新執行 useEffect

使用 useMemo 來記憶物件,useEffect 的 deps 若是 useMemo 後的物件,Re-Render 時,因為物件有被記憶住,所以不會重新執行 useEffect。

function App() {
  console.log('render');
  const [value, setValue] = React.useState(false);
  // 記住回傳物件值的useMemo
  const memoObj = React.useMemo(() => {
    const obj = { name: 'bruce', data: {} };
    return obj
  }, [])
  React.useEffect(() => { 
    console.log('useEffect cb');
  }, [memoObj]);
  
  return <>
    <h1>App</h1>
    <button onClick={() => {setValue(!value)}}>重新Render</button>
  </>
}

執行結果:https://codepen.io/lala-lee-jobs/pen/NWMdZpz?editors=0011

使用 useCallback 記憶 Function,避免重新執行 useEffect

如果 useEffect 的 deps 是 Function,而 Function 視為是物件的一種,除了可以使用 useMemo 來記憶,也可以使用 useCallback。

useCallback 是 useMemo 的一種變體,用來記住 Function,useCallback 其實就等於回傳一個 Function 的 useMemo。

記住 Function 使用 useMemo

使用 useMemo 來記憶 Function (視為物件),useEffect 的 deps 若是 useMemo 後的物件,重新 Render 時,因為物件有被記憶住,所以不會重新執行 useEffect

function App() {
  console.log('render');
  const [value, setValue] = React.useState(false);
  // 記住 回傳物件值的 useMemo,這邊回傳的是 Function
  const memoFunction = React.useMemo(() => {
    return () => {} // Retun Function Object
  }, [])
  React.useEffect(() => { 
    console.log('useEffect cb');
  }, [memoFunction]);

  return <>
    <h1>App</h1>
    <button onClick={() => {setValue(!value)}}>重新Render</button>
  </>
}

執行結果:https://codepen.io/lala-lee-jobs/pen/OJZWeoE?editors=0011

記住 Function 使用 useCallback

使用 useCallback 來記憶 Functon,useEffect 的 deps 若是useCallback 後的 Function,重新 Render 時,因為 Function 有被記憶住,所以不會重新執行 useEffect

function App() {
  console.log('render');
  const [value, setValue] = React.useState(false);  
  // 記住 Function 的 useCallback
  const memoFunction = React.useCallback(
    () => { console.log('memoFunction here'); }
  , [])
  React.useEffect(() => { 
    console.log('useEffect cb');
    memoFunction();
  }, [memoFunction]);

  return <>
    <h1>App</h1>
    <button onClick={() => {setValue(!value)}}>重新Render</button>
  </>
}

執行結果:https://codepen.io/lala-lee-jobs/pen/LYmxwNz?editors=0011

再探 useMemo

語法

const memoizedValue = useMemo(callback-fn, dependency-array);
  • dependency-array 只有這個陣列裡的變數更新,才會執行 useMemo 的 callback function,若陣列為空則表示初始執行一次。
  • callback-fn 通常會放需要複雜計算的邏輯處理,讓它只有 dependency-array 的變數更新時才執行。
const memoizedValue = useMemo(() => {
  let result = xxxxx;
  // 在此處放置複雜計算的邏輯處理
  // ...
  return result; // 要回傳給 memoizedValue 的值
}, [obj1, obj2, ...]);

使用情境 - 只有在必要時才執行 useMemo 裡的複雜運算

function App() {
  console.log('render');
  // status 變動,雖然會觸發 Render,但 count 沒有變動不會執行 slowComputed
  const [status, setStatus] = React.useState(false);
  const [count, setCount] = React.useState(1);  
  // 只有 count 變動時,才會執行 slowComputed
  const slowComputed = React.useMemo(() => {
    console.log('slowComputed');
    let sum = 0;
    for (let i = 0; i < count * 100 ; i++) {
      sum += 1;
    }
    return sum;
  }, [count])

  return <>
    <div>Status:{String(status)}</div>
    <div>Count:{count}</div>
    <div>SlowComputed:{slowComputed}</div>
    <div>
      <button onClick={() => {setStatus(!status)}}>Change Status</button>
      <span> Status 變動,雖然會觸發 Render,但不會執行 slowComputed</span>
    </div>
    <div>
      <button onClick={() => {setCount(count+1)}}>Change Count</button>
      <span> Count 變動,會觸發 Render,也會執行 slowComputed</span>
    </div>
  </>
}

執行結果:https://codepen.io/lala-lee-jobs/pen/yLjMJOR?editors=0011

使用情境 - 使用 useMemo 記憶物件,避免重新執行 useEffect

function App() {
  console.log('render');
  // status 變動,雖然會觸發 Render,但 dark 沒有變動不會執行 useEffect
  const [status, setStatus] = React.useState(false);
  // 只有 dark 變動時,才會執行 useEffect
  const [dark, setDark] = React.useState(false);  
  const themeStyle = React.useMemo(() => {
    return {
      backgroundColor: dark ? 'black' : 'white',
      color: dark ? 'white' : 'black',
    }
  }, [dark]);
  
  React.useEffect(() => {
    console.log('useEffect callback')
  }, [themeStyle]);

  return <>
    <div>Status:{String(status)}</div>
    <div>Dark:{String(dark)}</div>
    <div>ThemeStyle:{JSON.stringify(themeStyle)}</div>
    <div>
      <button onClick={() => {setStatus(!status)}}>Change Status</button>
      <span> Status 變動,雖然會觸發 Render,但不會執行 useEffect</span>
    </div>
    <div>
      <button onClick={() => {setDark(!dark)}}>Change Dark</button>
      <span> Dark 變動,會觸發 Render,也會執行 useEffect</span>
    </div>
  </>
}

執行結果:https://codepen.io/lala-lee-jobs/pen/vYjxeyw?editors=0011

再探 useCallback

語法

const memoizedCallback = useCallback(callback-fn, dependency-array);
  • dependency-array 只有這個陣列裡的變數更新,才會執行 useCallback 的 callback function,若陣列為空則表示初始執行一次。
  • callback-fn 要被記住的函式,讓它只有 dependency-array 的變數更新時才執行。

v.s. useMemo

使用 useMemo 需要有回傳值的表達式,無論回傳的是 Object 或是 Function

使用 useCallback 不需要有回傳值的表達式,傳入的 Callback Function,就是要被記住的 Function

useCallback(fn, deps) 等同於 useMemo(() => fn, deps)

使用情境 - 使用 useCallback 記憶 Function,避免重新執行 useEffect

function App() {
  console.log('render');
  const [postId, setPostId] = React.useState(1);
  const [status, setStatus] = React.useState(false);
  
  const fetchDataWithUseCallback = React.useCallback(() => {
    const fetchingData = async () => {
      const fetchUrl = `https://jsonplaceholder.typicode.com/posts/${postId}`;
      const response = await fetch(fetchUrl);
      const data = await response.json(response);
      return data;
    };
    fetchingData();
  }, []);
  
  const fetchData = async () => {
    const fetchUrl = `https://jsonplaceholder.typicode.com/posts/${postId}`;
    const response = await fetch(fetchUrl);
    const data = await response.json(response);
    return data;
  };  
  
  React.useEffect(() => {
    console.log('execute fetchDataWithUseCallback in useEffect');
    fetchDataWithUseCallback();
  }, [fetchDataWithUseCallback]);
  
  React.useEffect(() => {
    console.log('execute fetchData in useEffect');
    fetchData();
  }, [fetchData]);

  return <>
    <div>Status:{String(status)}</div>
    <div>
      <button onClick={() => {setStatus(!status)}}>Change Status</button>
      <span> Status 變動,雖然會觸發 Render,但不會執行 useEffect</span>
    </div>
  </>
}

執行結果:https://codepen.io/lala-lee-jobs/pen/xxjqPRK?editors=0011

使用情境 - 通常與 React.memo 搭配用以記住傳入子元件的 Function

這個會在下一篇介紹 React.memo 的時候會一併說明

Next

useMemo 與 useCallback 都會用來「避免重新執行不該被再次執行的運算」,接下來要介紹的 React.memo 則是用以「減少不必要被更新的元件被重新渲染」。

Reference

https://medium.com/%E6%89%8B%E5%AF%AB%E7%AD%86%E8%A8%98/react-optimize-performance-using-memo-usecallback-usememo-a76b6b272df3

https://zh-hant.reactjs.org/docs/hooks-reference.html#usememo

https://zh-hant.reactjs.org/docs/hooks-reference.html#usecallback

https://ithelp.ithome.com.tw/articles/10269673

https://medium.com/hannah-lin/react-hook-%E7%AD%86%E8%A8%98-memorized-hook-usememo-usecallback-e08a5e1bc9a2#d7e1

https://hackmd.io/@yellow/react-hooks#useCallback-%E8%A8%98%E6%86%B6%E5%80%BC

https://ithelp.ithome.com.tw/m/articles/10270317

https://juejin.cn/post/7122027852492439565

https://ithelp.ithome.com.tw/articles/10225504


上一篇
Day 14 useId、useLayoutEffect
下一篇
Day 16 React.memo (HOC)
系列文
開始搞懂React生態系30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言